#include <cstddef>
#include <chrono>
#include <Windows.h>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <vector>
#include <queue>
#include <string>
#include <conio.h>
#include <optional>
#include <type_traits>

#include "BitInt.hh"

template <typename T>
struct remove_cvref {
    using type = ::std::remove_reference_t<::std::remove_cv_t<T>>;
};

template <typename T>
using remove_cvref_t = typename remove_cvref<T>::type;

bool wait(::std::chrono::milliseconds num) noexcept {
    static auto start = std::chrono::high_resolution_clock::now();
    auto end = std::chrono::high_resolution_clock::now();
    if (num <= std::chrono::duration_cast<std::chrono::milliseconds>(end - start)) {
        start = end;
        return true;
    }
    return false;
}

template<::std::size_t width, ::std::size_t height>
class Screen {
    using byte = uint8_t;
    ::std::vector<::std::vector<byte>> data;
public:
    Screen() noexcept {
        data.resize(height, ::std::vector<byte>(width, 0));
    }

    template<auto N, typename ::std::enable_if_t<::std::is_integral_v<remove_cvref_t<decltype(N)>>>* = nullptr>
    void setPixel(BitInt<N> x, BitInt<N> y, byte value) noexcept {
        // assert(x < width && y < height);
        this->data[y.num()][x.num()] = value;
    }

    void setPixel(::std::size_t x, ::std::size_t y, byte value) noexcept {
        // assert(x < width && y < height);
        this->data[y % height][x % width] = value;
    }

    template<auto N, typename ::std::enable_if_t<::std::is_integral_v<remove_cvref_t<decltype(N)>>>* = nullptr>
    byte getPixel(BitInt<N> x, BitInt<N> y) const noexcept {
        // assert(x < width && y < height);
        return this->data[y.num()][x.num()];
    }

    void display() const noexcept {
        ::std::system("cls");
        auto output = ::std::string{};

        for (auto y = this->data.rbegin(); y != this->data.rend(); ++y) {
            for (auto x : *y) {
                output += ::std::to_string(x);
                output += ' ';
            }
            output += '\n';
        }

        ::std::cout << output << '\n';
    }
};

 enum class Derection {
	left = VK_LEFT,
	up = VK_UP,
	right = VK_RIGHT,
	down = VK_DOWN
};

::std::optional<Derection> keybord_getch() noexcept {
    static bool is_clicked{false};
    if (_kbhit()) {
        if (GetAsyncKeyState(VK_LEFT)) {
            if (is_clicked) {
                return ::std::nullopt;
            }
            is_clicked = true;
            return Derection::left;
        }
        else if (GetAsyncKeyState(VK_UP)) {
            if (is_clicked) {
                return ::std::nullopt;
            }
            is_clicked = true;
            return Derection::up;
        }
        else if (GetAsyncKeyState(VK_RIGHT)) {
            if (is_clicked) {
                return ::std::nullopt;
            }
            is_clicked = true;
            return Derection::right;
        }
        else if (GetAsyncKeyState(VK_DOWN)) {
            if (is_clicked) {
                return ::std::nullopt;
            }
            is_clicked = true;
            return Derection::down;
        }
    }
    is_clicked = false;
    return ::std::nullopt;
}

::std::optional<Derection> update_derection(Derection& derection) noexcept {
    auto derection_cache = keybord_getch();
    if (derection_cache.has_value() && derection != derection_cache.value()
        && (derection == Derection::left && derection_cache.value() != Derection::right
            || derection == Derection::right && derection_cache.value() != Derection::left
            || derection == Derection::up && derection_cache.value() != Derection::down
            || derection == Derection::down && derection_cache.value() != Derection::up))
    {
        return derection_cache;
    }
    return ::std::nullopt;
}

/*
 * meaning of screen number:
 * 0 : empty
 * 1-14 : body or head of snake (the max number is head, which equals to snack_length)
 * 15 : food
 */

int main() noexcept {
    ::std::signal(SIGINT, [](int) { ::std::exit(0); });
    time_t t; srand((unsigned int)time(&t));

    constexpr int width = 4, height = 4;
    auto screen = Screen<width, height>{};

    ::std::queue<Derection> inputs;
    inputs.push(Derection::right);

    int snack_length = 2;

    screen.setPixel(0, 0, 1);
    screen.setPixel(1, 0, 2);

    BitInt<2> head_x{1}, head_y{0};

    while (true)
    {
        auto derection_cache = update_derection(inputs.back());
        if (derection_cache.has_value()) {
            inputs.push(derection_cache.value());
        }

        std::chrono::milliseconds m(1000);
        if (wait(m)) // main
        {
            screen.display();

            // snack move
            for (uint8_t _y{}; _y < height; ++_y)
            {
                for (uint8_t _x{}; _x < width; ++_x) {
                    BitInt<2> x(_x), y(_y);
                    auto value = screen.getPixel(x, y);

                    if (value != 0 && value != 15) {
                        screen.setPixel(x, y, screen.getPixel(x, y) - 1);
                    }

                    if (value == snack_length) {
                        head_x = x;
                        head_y = y;
                    }
                }
            }
#if 0
            if (derection == Derection::left) {
                std::cout << "left" << std::endl;
            }
            else if (derection == Derection::right) {
                std::cout << "right" << std::endl;
            }
            else if (derection == Derection::up) {
                std::cout << "up" << std::endl;
            }
            else if (derection == Derection::down) {
                std::cout << "down" << std::endl;
            }
#endif
            if (inputs.size() > 1) {
                inputs.pop();
            }

            auto& derection = inputs.front();

            if (derection == Derection::left) {
                --head_x;
            }
            else if (derection == Derection::right) {
                ++head_x;
            }
            else if (derection == Derection::up) {
                ++head_y;
            }
            else if (derection == Derection::down) {
                --head_y;
            }

            // head hit body
            if (screen.getPixel(head_x, head_y) != 0 || screen.getPixel(head_x, head_y) != 15) {
                ::std::cout << "game over" << ::std::endl;
                ::std::exit(0);
            }

            if (screen.getPixel(head_x, head_y) == 15) { // head eat food
                ++snack_length;
                
            }
            screen.setPixel(head_x, head_y, snack_length);
            // BitInt<2> food_x{rand()}, food_y{rand()};
            // while (screen.getPixel(food_x, food_y) != 0) {
            //     food_x = rand(), food_y = rand();
            // }
        }
    }

    return 0;
}